"""Widgets for displaying and editing pipeline parameters."""

from __future__ import annotations

import math
from typing import Callable, Dict, Optional

from PyQt5.QtCore import QEvent, Qt, pyqtSignal
from PyQt5.QtWidgets import (
    QCheckBox,
    QComboBox,
    QDoubleSpinBox,
    QFormLayout,
    QHBoxLayout,
    QLineEdit,
    QSlider,
    QSpinBox,
    QWidget,
)

from ..core.types import ParameterDefinition, PipelineStep


class ParameterControl(QWidget):
    valueChanged = pyqtSignal(object)

    def __init__(self, definition: ParameterDefinition, initial_value: object) -> None:
        super().__init__()
        self.definition = definition
        layout = QHBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(8)

        self._slider: Optional[QSlider] = None
        self._spin_widget: Optional[QWidget] = None
        self._scale = 1.0
        self._int_min = 0
        self._float_min = 0.0
        self._pending_value = initial_value
        self._last_emitted = initial_value

        if definition.type == "int":
            self._setup_int(layout, initial_value)
        elif definition.type == "float":
            self._setup_float(layout, initial_value)
        elif definition.type == "choice":
            self._setup_choice(layout, initial_value)
        elif definition.type == "bool":
            self._setup_bool(layout, initial_value)
        elif definition.type in {"text", "string"}:
            self._setup_text(layout, initial_value)

    def eventFilter(self, obj, event):  # type: ignore[override]
        if obj is self._slider and event is not None:
            if event.type() in (QEvent.MouseButtonRelease, QEvent.KeyRelease, QEvent.FocusOut):
                self._commit_pending()
        return super().eventFilter(obj, event)

    def _should_emit(self, value: object) -> bool:
        if isinstance(value, float) and isinstance(self._last_emitted, float):
            return not math.isclose(value, self._last_emitted, rel_tol=1e-9, abs_tol=1e-9)
        return value != self._last_emitted

    def _set_pending(self, value: object) -> None:
        self._pending_value = value

    def _commit_pending(self) -> None:
        if self._should_emit(self._pending_value):
            self._last_emitted = self._pending_value
            self.valueChanged.emit(self._pending_value)

    def _setup_int(self, layout: QHBoxLayout, value: object) -> None:
        minimum = int(self.definition.minimum or 0)
        maximum = int(self.definition.maximum or 100)
        step = int(self.definition.step or 1)
        self._int_min = minimum

        slider = QSlider(Qt.Horizontal)
        slider.setRange(0, max(0, maximum - minimum))
        slider.setSingleStep(step)
        slider.setPageStep(step)
        slider.installEventFilter(self)

        spin = QSpinBox()
        spin.setRange(minimum, maximum)
        spin.setSingleStep(step)

        self._slider = slider
        self._spin_widget = spin

        layout.addWidget(slider)
        layout.addWidget(spin)

        def sync_from_slider(val: int) -> None:
            real = val + minimum
            self._set_pending(real)
            spin.blockSignals(True)
            spin.setValue(real)
            spin.blockSignals(False)

        def sync_from_spin(val: int) -> None:
            slider.blockSignals(True)
            slider.setValue(val - minimum)
            slider.blockSignals(False)
            self._set_pending(val)

        slider.valueChanged.connect(sync_from_slider)
        spin.valueChanged.connect(sync_from_spin)
        spin.editingFinished.connect(lambda: self._commit_from_spin_int(spin))

        spin.setValue(int(value))
        self._set_pending(int(value))
        self._last_emitted = int(value)

    def _commit_from_spin_int(self, spin: QSpinBox) -> None:
        value = spin.value()
        self._set_pending(value)
        if self._slider is not None:
            self._slider.blockSignals(True)
            self._slider.setValue(value - self._int_min)
            self._slider.blockSignals(False)
        self._commit_pending()

    def _setup_float(self, layout: QHBoxLayout, value: object) -> None:
        minimum = float(self.definition.minimum or 0.0)
        maximum = float(self.definition.maximum or 1.0)
        step = float(self.definition.step or 0.1)
        decimals = self.definition.decimals or max(0, min(4, -int(math.log10(step)) if step < 1 else 0))
        scale = 10**decimals
        self._scale = scale
        self._float_min = minimum

        slider = QSlider(Qt.Horizontal)
        slider.setRange(0, int((maximum - minimum) * scale))
        slider.setSingleStep(max(1, int(step * scale)))
        slider.setPageStep(max(1, int(step * scale)))
        slider.installEventFilter(self)

        spin = QDoubleSpinBox()
        spin.setRange(minimum, maximum)
        spin.setSingleStep(step)
        spin.setDecimals(decimals)

        self._slider = slider
        self._spin_widget = spin

        layout.addWidget(slider)
        layout.addWidget(spin)

        def sync_from_slider(val: int) -> None:
            real = minimum + val / scale
            self._set_pending(real)
            spin.blockSignals(True)
            spin.setValue(real)
            spin.blockSignals(False)

        def sync_from_spin(val: float) -> None:
            slider.blockSignals(True)
            slider.setValue(int((val - minimum) * scale))
            slider.blockSignals(False)
            self._set_pending(val)

        slider.valueChanged.connect(sync_from_slider)
        spin.valueChanged.connect(sync_from_spin)
        spin.editingFinished.connect(lambda: self._commit_from_spin_float(spin))

        spin.setValue(float(value))
        self._set_pending(float(value))
        self._last_emitted = float(value)

    def _commit_from_spin_float(self, spin: QDoubleSpinBox) -> None:
        value = spin.value()
        self._set_pending(value)
        if self._slider is not None:
            slider_val = int((value - self._float_min) * self._scale)
            slider_val = max(0, min(slider_val, self._slider.maximum()))
            self._slider.blockSignals(True)
            self._slider.setValue(slider_val)
            self._slider.blockSignals(False)
        self._commit_pending()

    def _setup_choice(self, layout: QHBoxLayout, value: object) -> None:
        combo = QComboBox()
        for text, data in self.definition.choices or []:
            combo.addItem(text, data)
        idx = combo.findData(value)
        if idx >= 0:
            combo.setCurrentIndex(idx)

        def emit_choice() -> None:
            current = combo.currentData()
            self._set_pending(current)
            if self._should_emit(current):
                self._last_emitted = current
                self.valueChanged.emit(current)

        combo.currentIndexChanged.connect(lambda _: emit_choice())
        layout.addWidget(combo)
        self._spin_widget = combo
        self._last_emitted = combo.currentData()
        self._pending_value = self._last_emitted

    def _setup_bool(self, layout: QHBoxLayout, value: object) -> None:
        checkbox = QCheckBox()
        checkbox.setChecked(bool(value))

        def emit_state(state: int) -> None:
            current = bool(state)
            self._set_pending(current)
            if self._should_emit(current):
                self._last_emitted = current
                self.valueChanged.emit(current)

        checkbox.stateChanged.connect(emit_state)
        layout.addWidget(checkbox)
        self._spin_widget = checkbox
        self._last_emitted = bool(value)
        self._pending_value = bool(value)

    def _setup_text(self, layout: QHBoxLayout, value: object) -> None:
        line_edit = QLineEdit()
        text_value = "" if value is None else str(value)
        line_edit.setText(text_value)
        self._spin_widget = line_edit
        self._last_emitted = text_value
        self._pending_value = text_value

        def commit_text() -> None:
            current = line_edit.text()
            self._set_pending(current)
            if self._should_emit(current):
                self._last_emitted = current
                self.valueChanged.emit(current)

        line_edit.editingFinished.connect(commit_text)
        layout.addWidget(line_edit)

    def set_value(self, value: object) -> None:
        self._pending_value = value
        self._last_emitted = value
        if isinstance(self._spin_widget, QSpinBox):
            spin = self._spin_widget
            spin.blockSignals(True)
            spin.setValue(int(value))
            spin.blockSignals(False)
            if self._slider is not None:
                slider_val = int(value) - self._int_min
                slider_val = max(0, min(slider_val, self._slider.maximum()))
                self._slider.blockSignals(True)
                self._slider.setValue(slider_val)
                self._slider.blockSignals(False)
        elif isinstance(self._spin_widget, QDoubleSpinBox):
            spin = self._spin_widget
            spin.blockSignals(True)
            spin.setValue(float(value))
            spin.blockSignals(False)
            if self._slider is not None:
                slider_val = int((float(value) - self._float_min) * self._scale)
                slider_val = max(0, min(slider_val, self._slider.maximum()))
                self._slider.blockSignals(True)
                self._slider.setValue(slider_val)
                self._slider.blockSignals(False)
        elif isinstance(self._spin_widget, QComboBox):
            idx = self._spin_widget.findData(value)
            if idx >= 0:
                self._spin_widget.blockSignals(True)
                self._spin_widget.setCurrentIndex(idx)
                self._spin_widget.blockSignals(False)
        elif isinstance(self._spin_widget, QCheckBox):
            self._spin_widget.blockSignals(True)
            self._spin_widget.setChecked(bool(value))
            self._spin_widget.blockSignals(False)
        elif isinstance(self._spin_widget, QLineEdit):
            text_value = "" if value is None else str(value)
            self._spin_widget.blockSignals(True)
            self._spin_widget.setText(text_value)
            self._spin_widget.blockSignals(False)
            self._pending_value = text_value
            self._last_emitted = text_value


class ParameterPanel(QWidget):
    def __init__(self) -> None:
        super().__init__()
        self.layout = QFormLayout(self)
        self.layout.setLabelAlignment(Qt.AlignLeft)
        self.layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
        self.current_controls: Dict[str, ParameterControl] = {}

    def set_parameters(
        self,
        step: Optional[PipelineStep],
        on_value_changed: Callable[[str, object], None],
    ) -> None:
        for i in reversed(range(self.layout.count())):
            item = self.layout.takeAt(i)
            if item.widget():
                item.widget().deleteLater()
        self.current_controls.clear()

        if step is None:
            return

        for param in step.definition.parameters:
            control = ParameterControl(param, step.params.get(param.name, param.default))
            control.valueChanged.connect(lambda value, name=param.name: on_value_changed(name, value))
            self.layout.addRow(param.label + ":", control)
            self.current_controls[param.name] = control
